/*
 * Copyright (C) 2011 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.google.dexmaker;

import static com.android.dx.rop.code.AccessFlags.ACC_CONSTRUCTOR;
import static java.lang.reflect.Modifier.PRIVATE;
import static java.lang.reflect.Modifier.STATIC;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.reflect.Modifier;
import java.util.Enumeration;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.jar.JarEntry;
import java.util.jar.JarOutputStream;
import java.util.zip.ZipFile;

import android.app.Application;

import com.android.dx.dex.DexFormat;
import com.android.dx.dex.DexOptions;
import com.android.dx.dex.code.DalvCode;
import com.android.dx.dex.code.PositionList;
import com.android.dx.dex.code.RopTranslator;
import com.android.dx.dex.file.ClassDefItem;
import com.android.dx.dex.file.DexFile;
import com.android.dx.dex.file.EncodedField;
import com.android.dx.dex.file.EncodedMethod;
import com.android.dx.rop.code.AccessFlags;
import com.android.dx.rop.code.LocalVariableInfo;
import com.android.dx.rop.code.RopMethod;
import com.android.dx.rop.cst.CstString;
import com.android.dx.rop.cst.CstType;
import com.android.dx.rop.type.StdTypeList;

import dalvik.system.DexClassLoader;

/**
 * Generates a </i><strong>D</strong>alvik <strong>EX</strong>ecutable (dex) file for execution on Android. Dex files define classes and interfaces, including their member methods and fields, executable code, and debugging information. They also define annotations, though this API currently has no facility to create a dex file that contains annotations.
 * 
 * <p>
 * This library is intended to satisfy two use cases:
 * <ul>
 * <li><strong>For runtime code generation.</strong> By embedding this library in your Android application, you can dynamically generate and load executable code. This approach takes advantage of the fact that the host environment and target environment are both Android.
 * <li><strong>For compile time code generation.</strong> You may use this library as a part of a compiler that targets Android. In this scenario the generated dex file must be installed on an Android device before it can be executed.
 * </ul>
 * 
 * <h3>Example: Fibonacci</h3>
 * To illustrate how this API is used, we'll use DexMaker to generate a class equivalent to the following Java source:
 * 
 * <pre>
 * {@code
 * 
 * package com.publicobject.fib;
 * 
 * public class Fibonacci {
 *   public static int fib(int i) {
 *     if (i < 2) {
 *       return i;
 *     }
 *     return fib(i - 1) + fib(i - 2);
 *   }
 * }}
 * </pre>
 * 
 * <p>
 * We start by creating a {@link TypeId} to identify the generated {@code Fibonacci} class. DexMaker identifies types by their internal names like {@code Ljava/lang/Object;} rather than their Java identifiers like {@code java.lang.Object}.
 * 
 * <pre>
 * {
 * 	&#064;code
 * 	TypeId&lt;?&gt; fibonacci = TypeId.get(&quot;Lcom/google/dexmaker/examples/Fibonacci;&quot;);
 * }
 * </pre>
 * 
 * <p>
 * Next we declare the class. It allows us to specify the type's source file for stack traces, its modifiers, its superclass, and the interfaces it implements. In this case, {@code Fibonacci} is a public class that extends from {@code Object}:
 * 
 * <pre>
 * {
 * 	&#064;code
 * 	String fileName = &quot;Fibonacci.generated&quot;;
 * 	DexMaker dexMaker = new DexMaker();
 * 	dexMaker.declare(fibonacci, fileName, Modifier.PUBLIC, TypeId.OBJECT);
 * }
 * </pre>
 * 
 * It is illegal to declare members of a class without also declaring the class itself.
 * 
 * <p>
 * To make it easier to go from our Java method to dex instructions, we'll manually translate it to pseudocode fit for an assembler. We need to replace control flow like {@code if()} blocks and {@code for()} loops with labels and branches. We'll also avoid performing multiple operations in one statement, using local variables to hold intermediate values as necessary:
 * 
 * <pre>
 * {@code
 * 
 *   int constant1 = 1;
 *   int constant2 = 2;
 *   if (i < constant2) goto baseCase;
 *   int a = i - constant1;
 *   int b = i - constant2;
 *   int c = fib(a);
 *   int d = fib(b);
 *   int result = c + d;
 *   return result;
 * baseCase:
 *   return i;
 * }
 * </pre>
 * 
 * <p>
 * We look up the {@code MethodId} for the method on the declaring type. This takes the method's return type (possibly {@link TypeId#VOID}), its name and its parameters types. Next we declare the method, specifying its modifiers by bitwise ORing constants from {@link java.lang.reflect.Modifier}. The declare call returns a {@link Code} object, which we'll use to define the method's instructions.
 * 
 * <pre>
 * {
 * 	&#064;code
 * 	MethodId&lt;?, Integer&gt; fib = fibonacci.getMethod(TypeId.INT, &quot;fib&quot;, TypeId.INT);
 * 	Code code = dexMaker.declare(fib, Modifier.PUBLIC | Modifier.STATIC);
 * }
 * </pre>
 * 
 * <p>
 * One limitation of {@code DexMaker}'s API is that it requires all local variables to be created before any instructions are emitted. Use {@link Code#newLocal newLocal()} to create a new local variable. The method's parameters are exposed as locals using {@link Code#getParameter getParameter()}. For non-static methods the {@code this} pointer is exposed using {@link Code#getThis getThis()}. Here we declare all of the local variables that we'll need for our {@code fib()} method:
 * 
 * <pre>
 * {
 * 	&#064;code
 * 	Local&lt;Integer&gt; i = code.getParameter(0, TypeId.INT);
 * 	Local&lt;Integer&gt; constant1 = code.newLocal(TypeId.INT);
 * 	Local&lt;Integer&gt; constant2 = code.newLocal(TypeId.INT);
 * 	Local&lt;Integer&gt; a = code.newLocal(TypeId.INT);
 * 	Local&lt;Integer&gt; b = code.newLocal(TypeId.INT);
 * 	Local&lt;Integer&gt; c = code.newLocal(TypeId.INT);
 * 	Local&lt;Integer&gt; d = code.newLocal(TypeId.INT);
 * 	Local&lt;Integer&gt; result = code.newLocal(TypeId.INT);
 * }
 * </pre>
 * 
 * <p>
 * Notice that {@link Local} has a type parameter of {@code Integer}. This is useful for generating code that works with existing types like {@code String} and {@code Integer}, but it can be a hindrance when generating code that involves new types. For this reason you may prefer to use raw types only and add {@code @SuppressWarnings("unsafe")} on your calling code. This will yield the same result but you won't get IDE support if you make a type error.
 * 
 * <p>
 * We're ready to start defining our method's instructions. The {@link Code} class catalogs the available instructions and their use.
 * 
 * <pre>
 * {@code
 * 
 *   code.loadConstant(constant1, 1);
 *   code.loadConstant(constant2, 2);
 *   Label baseCase = new Label();
 *   code.compare(Comparison.LT, baseCase, i, constant2);
 *   code.op(BinaryOp.SUBTRACT, a, i, constant1);
 *   code.op(BinaryOp.SUBTRACT, b, i, constant2);
 *   code.invokeStatic(fib, c, a);
 *   code.invokeStatic(fib, d, b);
 *   code.op(BinaryOp.ADD, result, c, d);
 *   code.returnValue(result);
 *   code.mark(baseCase);
 *   code.returnValue(i);
 * }
 * </pre>
 * 
 * <p>
 * We're done defining the dex file. We just need to write it to the filesystem or load it into the current process. For this example we'll load the generated code into the current process. This only works when the current process is running on Android. We use {@link #generateAndLoad generateAndLoad()} which takes the class loader that will be used as our generated code's parent class loader. It also requires a directory where temporary files can be written.
 * 
 * <pre>
 * {
 * 	&#064;code
 * 	ClassLoader loader = dexMaker.generateAndLoad(FibonacciMaker.class.getClassLoader(), getDataDirectory());
 * }
 * </pre>
 * 
 * Finally we'll use reflection to lookup our generated class on its class loader and invoke its {@code fib()} method:
 * 
 * <pre>
 * {
 * 	&#064;code
 * 	Class&lt;?&gt; fibonacciClass = loader.loadClass(&quot;com.google.dexmaker.examples.Fibonacci&quot;);
 * 	Method fibMethod = fibonacciClass.getMethod(&quot;fib&quot;, int.class);
 * 	System.out.println(fibMethod.invoke(null, 8));
 * }
 * </pre>
 */
public final class DexMaker {
	private final Map<TypeId<?>, TypeDeclaration> types = new LinkedHashMap<TypeId<?>, TypeDeclaration>();
	private Application application;

	/**
	 * Creates a new {@code DexMaker} instance, which can be used to create a single dex file.
	 */
	public DexMaker(Application application) {
		this.application = application;
	}

	private TypeDeclaration getTypeDeclaration(TypeId<?> type) {
		TypeDeclaration result = types.get(type);
		if (result == null) {
			result = new TypeDeclaration(type);
			types.put(type, result);
		}
		return result;
	}

	/**
	 * Declares {@code type}.
	 * 
	 * @param flags
	 *            a bitwise combination of {@link Modifier#PUBLIC}, {@link Modifier#FINAL} and {@link Modifier#ABSTRACT}.
	 */
	public void declare(TypeId<?> type, String sourceFile, int flags, TypeId<?> supertype, TypeId<?>... interfaces) {
		TypeDeclaration declaration = getTypeDeclaration(type);
		int supportedFlags = Modifier.PUBLIC | Modifier.FINAL | Modifier.ABSTRACT;
		if ((flags & ~supportedFlags) != 0) {
			throw new IllegalArgumentException("Unexpected flag: " + Integer.toHexString(flags));
		}
		if (declaration.declared) {
			throw new IllegalStateException("already declared: " + type);
		}
		declaration.declared = true;
		declaration.flags = flags;
		declaration.supertype = supertype;
		declaration.sourceFile = sourceFile;
		declaration.interfaces = new TypeList(interfaces);
	}

	/**
	 * Declares a method or constructor.
	 * 
	 * @param flags
	 *            a bitwise combination of {@link Modifier#PUBLIC}, {@link Modifier#PRIVATE}, {@link Modifier#PROTECTED}, {@link Modifier#STATIC}, {@link Modifier#FINAL} and {@link Modifier#SYNCHRONIZED}.
	 *            <p>
	 *            <strong>Warning:</strong> the {@link Modifier#SYNCHRONIZED} flag is insufficient to generate a synchronized method. You must also use {@link Code#monitorEnter} and {@link Code#monitorExit} to acquire a monitor.
	 */
	public Code declare(MethodId<?, ?> method, int flags) {
		TypeDeclaration typeDeclaration = getTypeDeclaration(method.declaringType);
		if (typeDeclaration.methods.containsKey(method)) {
			throw new IllegalStateException("already declared: " + method);
		}

		int supportedFlags = Modifier.PUBLIC | Modifier.PRIVATE | Modifier.PROTECTED | Modifier.STATIC | Modifier.FINAL | Modifier.SYNCHRONIZED;
		if ((flags & ~supportedFlags) != 0) {
			throw new IllegalArgumentException("Unexpected flag: " + Integer.toHexString(flags));
		}

		// replace the SYNCHRONIZED flag with the DECLARED_SYNCHRONIZED flag
		if ((flags & Modifier.SYNCHRONIZED) != 0) {
			flags = (flags & ~Modifier.SYNCHRONIZED) | AccessFlags.ACC_DECLARED_SYNCHRONIZED;
		}

		if (method.isConstructor()) {
			flags |= ACC_CONSTRUCTOR;
		}

		MethodDeclaration methodDeclaration = new MethodDeclaration(method, flags);
		typeDeclaration.methods.put(method, methodDeclaration);
		return methodDeclaration.code;
	}

	/**
	 * Declares a field.
	 * 
	 * @param flags
	 *            a bitwise combination of {@link Modifier#PUBLIC}, {@link Modifier#PRIVATE}, {@link Modifier#PROTECTED}, {@link Modifier#STATIC}, {@link Modifier#FINAL}, {@link Modifier#VOLATILE}, and {@link Modifier#TRANSIENT}.
	 * @param staticValue
	 *            a constant representing the initial value for the static field, possibly null. This must be null if this field is non-static.
	 */
	public void declare(FieldId<?, ?> fieldId, int flags, Object staticValue) {
		TypeDeclaration typeDeclaration = getTypeDeclaration(fieldId.declaringType);
		if (typeDeclaration.fields.containsKey(fieldId)) {
			throw new IllegalStateException("already declared: " + fieldId);
		}

		int supportedFlags = Modifier.PUBLIC | Modifier.PRIVATE | Modifier.PROTECTED | Modifier.STATIC | Modifier.FINAL | Modifier.VOLATILE | Modifier.TRANSIENT;
		if ((flags & ~supportedFlags) != 0) {
			throw new IllegalArgumentException("Unexpected flag: " + Integer.toHexString(flags));
		}

		if ((flags & Modifier.STATIC) == 0 && staticValue != null) {
			throw new IllegalArgumentException("staticValue is non-null, but field is not static");
		}

		FieldDeclaration fieldDeclaration = new FieldDeclaration(fieldId, flags, staticValue);
		typeDeclaration.fields.put(fieldId, fieldDeclaration);
	}

	/**
	 * Generates a dex file and returns its bytes.
	 */
	public byte[] generate() {
		DexOptions options = new DexOptions();
		options.targetApiLevel = DexFormat.API_NO_EXTENDED_OPCODES;
		DexFile outputDex = new DexFile(options);

		for (TypeDeclaration typeDeclaration : types.values()) {
			outputDex.add(typeDeclaration.toClassDefItem());
		}

		try {
			return outputDex.toDex(null, false);
		} catch (IOException e) {
			throw new RuntimeException(e);
		}
	}

	/**
	 * Generates a dex file and loads its types into the current process.
	 * 
	 * <h3>Picking a dex cache directory</h3> The {@code dexCache} should be an application-private directory. If you pass a world-writable directory like {@code /sdcard} a malicious app could inject code into your process. Most applications should use this:
	 * 
	 * <pre>
	 * {
	 * 	&#064;code
	 * 	File dexCache = getApplicationContext().getDir(&quot;dx&quot;, Context.MODE_PRIVATE);
	 * }
	 * </pre>
	 * 
	 * If the {@code dexCache} is null, this method will consult the {@code dexmaker.dexcache} system property. If that exists, it will be used for the dex cache. If it doesn't exist, this method will attempt to guess the application's private data directory as a last resort. If that fails, this method will fail with an unchecked exception. You can avoid the exception by either providing a non-null value or setting the system property.
	 * 
	 * @param parent
	 *            the parent ClassLoader to be used when loading our generated types
	 * @param dexCache
	 *            the destination directory where generated and optimized dex files will be written. If null, this class will try to guess the application's private data dir.
	 */
	public ClassLoader generateAndLoad(ClassLoader parent, File dexCache, String jarName) {
		if (dexCache == null) {
			String property = System.getProperty("dexmaker.dexcache");
			if (property != null) {
				dexCache = new File(property);
			} else {
				dexCache = new AppDataDirGuesser().guess();
				if (dexCache == null) {
					throw new IllegalArgumentException("dexcache == null (and no default could be" + " found; consider setting the 'dexmaker.dexcache' system property)");
				}
			}
		}
		/*
		 * This implementation currently dumps the dex to the filesystem. It jars the emitted .dex for the benefit of Gingerbread and earlier devices, which can't load .dex files directly.
		 * 
		 * TODO: load the dex from memory where supported.
		 */
		File result = new File(dexCache, jarName + ".jar");
		if (!result.exists()) {
			JarOutputStream jarOut = null;
			try {
				jarOut = new JarOutputStream(new FileOutputStream(result));
				jarOut.putNextEntry(new JarEntry(DexFormat.DEX_IN_JAR_NAME));
				byte[] dex = generate();
				jarOut.write(dex);
				jarOut.closeEntry();
				jarOut.close();
			} catch (Exception e) {
				e.printStackTrace();
				try {
					if (jarOut != null) {
						jarOut.close();
					}
				} catch (IOException ex) {
				}
			}
		}

		DexClassLoader dexClassLoader = new DexClassLoader(result.getPath(), application.getApplicationInfo().dataDir, null, parent);
		return dexClassLoader;
	}

	public ClassLoader getJarClassLoader(ClassLoader parent, File dexCache, String jarName) throws IOException {
		if (dexCache == null) {
			String property = System.getProperty("dexmaker.dexcache");
			if (property != null) {
				dexCache = new File(property);
			} else {
				dexCache = new AppDataDirGuesser().guess();
				if (dexCache == null) {
					throw new IllegalArgumentException("dexcache == null (and no default could be" + " found; consider setting the 'dexmaker.dexcache' system property)");
				}
			}
		}
		File result = new File(dexCache, jarName + ".jar");
		if (!result.exists() || result.length() == 0) {
			return null;
		}
		DexClassLoader dexClassLoader = new DexClassLoader(result.getPath(), application.getApplicationInfo().dataDir, null, parent);
		return dexClassLoader;
	}

	private static class TypeDeclaration {
		private final TypeId<?> type;

		/** declared state */
		private boolean declared;
		private int flags;
		private TypeId<?> supertype;
		private String sourceFile;
		private TypeList interfaces;

		private final Map<FieldId, FieldDeclaration> fields = new LinkedHashMap<FieldId, FieldDeclaration>();
		private final Map<MethodId, MethodDeclaration> methods = new LinkedHashMap<MethodId, MethodDeclaration>();

		TypeDeclaration(TypeId<?> type) {
			this.type = type;
		}

		ClassDefItem toClassDefItem() {
			if (!declared) {
				throw new IllegalStateException("Undeclared type " + type + " declares members: " + fields.keySet() + " " + methods.keySet());
			}

			DexOptions dexOptions = new DexOptions();
			dexOptions.targetApiLevel = DexFormat.API_NO_EXTENDED_OPCODES;

			CstType thisType = type.constant;

			ClassDefItem out = new ClassDefItem(thisType, flags, supertype.constant, interfaces.ropTypes, new CstString(sourceFile));

			for (MethodDeclaration method : methods.values()) {
				EncodedMethod encoded = method.toEncodedMethod(dexOptions);
				if (method.isDirect()) {
					out.addDirectMethod(encoded);
				} else {
					out.addVirtualMethod(encoded);
				}
			}
			for (FieldDeclaration field : fields.values()) {
				EncodedField encoded = field.toEncodedField();
				if (field.isStatic()) {
					out.addStaticField(encoded, Constants.getConstant(field.staticValue));
				} else {
					out.addInstanceField(encoded);
				}
			}

			return out;
		}
	}

	static class FieldDeclaration {
		final FieldId<?, ?> fieldId;
		private final int accessFlags;
		private final Object staticValue;

		FieldDeclaration(FieldId<?, ?> fieldId, int accessFlags, Object staticValue) {
			if ((accessFlags & STATIC) == 0 && staticValue != null) {
				throw new IllegalArgumentException("instance fields may not have a value");
			}
			this.fieldId = fieldId;
			this.accessFlags = accessFlags;
			this.staticValue = staticValue;
		}

		EncodedField toEncodedField() {
			return new EncodedField(fieldId.constant, accessFlags);
		}

		public boolean isStatic() {
			return (accessFlags & STATIC) != 0;
		}
	}

	static class MethodDeclaration {
		final MethodId<?, ?> method;
		private final int flags;
		private final Code code;

		public MethodDeclaration(MethodId<?, ?> method, int flags) {
			this.method = method;
			this.flags = flags;
			this.code = new Code(this);
		}

		boolean isStatic() {
			return (flags & STATIC) != 0;
		}

		boolean isDirect() {
			return (flags & (STATIC | PRIVATE | ACC_CONSTRUCTOR)) != 0;
		}

		EncodedMethod toEncodedMethod(DexOptions dexOptions) {
			RopMethod ropMethod = new RopMethod(code.toBasicBlocks(), 0);
			LocalVariableInfo locals = null;
			DalvCode dalvCode = RopTranslator.translate(ropMethod, PositionList.NONE, locals, code.paramSize(), dexOptions);
			return new EncodedMethod(method.constant, flags, dalvCode, StdTypeList.EMPTY);
		}
	}
}
